雖然 Vue 的聲明性渲染模型為你抽象了大部分對 DOM 的直接操作,但在某些情況下,我們仍然需要直接訪問底層 DOM 元素。要實現這一點,我們可以使用特殊的 ref attribute:
<input ref="input">
ref 是一個特殊的 attribute,和 v-for 章節中提到的 key 類似。它允許我們在一個特定的 DOM 元素或子組件實例被掛載後,獲得對它的直接引用。這可能很有用,例如說在組件掛載時將焦點設置到一個 input 元素上,或在一個元素上初始化一個第三方庫。
為了通過組合式 API 獲得該模板引用,我們需要聲明一個匹配模板 ref attribute 值的 ref:
<script setup>
import { ref, onMounted } from 'vue'
// 聲明一個 ref 來存放該元素的引用
// 必須和模板裡的 ref 同名
const input = ref(null)
onMounted(() => {
input.value.focus()
})
</script>
<template>
<input ref="input" />
</template>
如果不使用 script setup,需確保從 setup() 返回 ref:
export default {
setup() {
const input = ref(null)
// ...
return {
input
}
}
}
注意,你只可以在組件掛載後才能訪問模板引用。如果你想在模板中的表達式上訪問 input,在初次渲染時會是 null,因為在初次渲染前這個元素還不存在!
如果你需要偵聽一個模板引用 ref 的變化,確保考慮到其值為 null 的情況:
watchEffect(() => {
if (input.value) {
input.value.focus()
} else {
// 此時還未掛載,或此元素已經被卸載(例如通過 v-if 控制)
}
})
需要 v3.2.25 及以上版本
當在 v-for 中使用模板引用時,對應的 ref 中包含的值是一個數組,它將在元素被掛載後包含對應整個列表的所有元素:
<script setup>
import { ref, onMounted } from 'vue'
const list = ref([
/* ... */
])
const itemRefs = ref([])
onMounted(() => console.log(itemRefs.value))
</script>
<template>
<ul>
<li v-for="item in list" ref="itemRefs">
{{ item }}
</li>
</ul>
</template>
應該注意的是,ref 數組並不保證與源數組相同的順序。
除了使用字符串值作名字,ref attribute 還可以綁定為一個函數,會在每次組件更新時都被調用。該函數會收到元素引用作為其第一個參數:
<input :ref="(el) => { /* 將 el 賦值給一個數據屬性或 ref 變量 */ }">
注意我們這裡需要使用動態的 :ref 綁定才能夠傳入一個函數。當綁定的元素被卸載時,函數也會被調用一次,此時的 el 參數會是 null。你當然也可以綁定一個組件方法而不是內聯函數。
模板引用也可以被用在一個子組件上。這種情況下引用中獲得的值是組件實例:
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'
const child = ref(null)
onMounted(() => {
// child.value 是 <Child /> 組件的實例
})
</script>
<template>
<Child ref="child" />
</template>
如果一個子組件使用的是選項式 API 或沒有使用 script setup,被引用的組件實例和該子組件的 this 完全一致,這意味著父組件對子組件的每一個屬性和方法都有完全的訪問權。這使得在父組件和子組件之間創建緊密耦合的實現細節變得很容易,當然也因此,應該只在絕對需要時才使用組件引用。大多數情況下,你應該首先使用標準的 props 和 emit 接口來實現父子組件交互。
有一個例外的情況,使用了 script setup 的組件是默認私有的:一個父組件無法訪問到一個使用了 script setup 的子組件中的任何東西,除非子組件在其中通過 defineExpose 宏顯式暴露:
<script setup>
import { ref } from 'vue'
const a = 1
const b = ref(2)
// 像 defineExpose 這樣的編譯器宏不需要導入
defineExpose({
a,
b
})
</script>
當父組件通過模板引用獲取到了該組件的實例時,得到的實例類型為 { a: number, b: number } (ref 都會自動解包,和一般的實例一樣)。